import { EventEmitter, IEventListener } from './common/event-emmiter';
import { World } from './ecs/world';

export class Engine {
  #isFinished: boolean = false;
  #isWorldStarted: boolean = false;
  #world: World | null = null;
  #worldListeners: IEventListener[] = [];

  #animationRequestId: number | null = null;
  #lastTimeStamp: number | null = null;

  readonly onLoading = new EventEmitter<boolean>();

  get isRunning(): boolean {
    return this.#animationRequestId !== null;
  }

  get isFinished(): boolean {
    return this.#isFinished;
  }

  get isVisible(): boolean {
    return document.visibilityState === 'visible';
  }

  attachWorld(world: World) {
    if (this.isRunning) {
      throw new Error('Cannot attach world while engine is running');
    }

    this.#world = world;
    this.#isWorldStarted = false;

    this.#worldListeners.push(this.#world.onLoading.listen((isLoading) => this.onLoading.emit(isLoading)));
    this.listenToVisibilityChange();
  }

  detachWorld() {
    if (this.isRunning) {
      throw new Error('Cannot detach world while engine is running');
    }

    this.#world = null;
    this.#isWorldStarted = false;

    this.#worldListeners.forEach((listener) => listener.dispose());
    this.#worldListeners.splice(0, this.#worldListeners.length);
    this.stopListeningToVisibilityChange();
  }

  async start() {
    if (!this.isVisible) {
      return;
    }

    if (this.isRunning) {
      return;
    }

    if (!this.#world) {
      throw new Error('No world attached to engine');
    }

    if (!this.#isWorldStarted) {
      this.#isWorldStarted = true;

      await this.#world.start();
    }

    this.loop();
  }

  stop() {
    if (!this.isRunning) {
      return;
    }

    cancelAnimationFrame(this.#animationRequestId!);

    this.#animationRequestId = null;
    this.#lastTimeStamp = null;
  }

  finish() {
    if (this.isFinished) {
      return;
    }

    if (this.isRunning) {
      this.stop();
    }

    this.#world?.finish();
    this.detachWorld();
  }

  private loop() {
    const now = performance.now();

    if (this.#lastTimeStamp !== null) {
      const dt = (now - this.#lastTimeStamp) / 1000;
      this.#lastTimeStamp = now;

      if (this.#world) {
        this.#world.update(dt);
      }

      /// после world.update внешний код может уничтожить world
      /// потому необходима дополнительная проверка
      if (this.#world) {
        this.#world.postUpdate(dt);

        this.#animationRequestId = requestAnimationFrame(() => this.loop());
      }
    } else {
      this.#lastTimeStamp = now;
      this.#animationRequestId = requestAnimationFrame(() => this.loop());
    }
  }

  private listenToVisibilityChange() {
    document.addEventListener('visibilitychange', this.handleVisibilityChange);
  }

  private stopListeningToVisibilityChange() {
    document.removeEventListener('visibilitychange', this.handleVisibilityChange);
  }

  private handleVisibilityChange = () => {
    if (this.isVisible) {
      this.start();
    } else {
      this.stop();
    }
  };
}
